@version=6
indicator(Liquidity Void Detector + Pro Signals, overlay=true, max_boxes_count=30, max_labels_count=25)

 === Inputs ===
voidBodyRatio = input.float(1.5, BodyWick Ratio For Void)
voidMinSize   = input.float(1.5, Min Candle Size vs ATR For Void)
voidLength    = input.int(20, Void Box Length (Bars))
momentumLen   = input.int(10, Momentum Confirm Length)
momentumThresh= input.float(0.8, Min Momentum Slope Signal)
sigSize       = input.string(tiny, Signal Label Size, options=[tiny, small])
dashPos       = input.string(top_right, Dashboard Position, options=[top_right, top_left, bottom_right, bottom_left])
showDashboard = input.bool(true, Show Probability Meter)
maxVoidBoxes  = input.int(10, Max Boxes On Chart, minval=1, maxval=50)

 === Candle Calculations & New Void Detection ===
upperWick    = high - math.max(open, close)
lowerWick    = math.min(open, close) - low
body         = math.abs(close - open)
avgWick      = (upperWick + lowerWick)  2
bodyWickRatio= avgWick  0  body  avgWick  0
candleATR    = ta.atr(10)
isVoid       = bodyWickRatio  voidBodyRatio and body  (candleATR  voidMinSize)

 === Arrays to Track Voids and State ===
var float[] voidHighs = array.new_float()
var float[] voidLows  = array.new_float()
var int[]   voidBars  = array.new_int()
var int[]   voidEnds  = array.new_int()
var box[]   voidBoxes = array.new_box()
var bool[]  voidLabeled = array.new_bool()

 === Only Add Non-Overlapping Boxes ===
shouldDrawBox = true
boxCount = array.size(voidBoxes)

if boxCount  0
    for i = 0 to boxCount - 1
        vb = array.get(voidBars, i)
        ve = array.get(voidEnds, i)
        vh = array.get(voidHighs, i)
        vl = array.get(voidLows, i)
        if bar_index = vb and bar_index = ve and high = vl and low = vh
            shouldDrawBox = false

if isVoid and shouldDrawBox
    array.push(voidHighs, high)
    array.push(voidLows, low)
    array.push(voidBars, bar_index)
    array.push(voidEnds, bar_index + voidLength)
    array.push(voidLabeled, false)
    newBox = box.new(bar_index, high, bar_index + voidLength, low, bgcolor=color.new(color.yellow, 80), border_color=color.new(color.orange, 0))
    array.push(voidBoxes, newBox)

 --- Prune oldest boxes and associated arrays ---
while array.size(voidBoxes)  maxVoidBoxes
    oldBox = array.shift(voidBoxes)
    box.delete(oldBox)
    array.shift(voidHighs)
    array.shift(voidLows)
    array.shift(voidBars)
    array.shift(voidLabeled)
    array.shift(voidEnds)

 === Momentum Filter and Clean Signal Labels (guarded) ===
momentumSlope = ta.change(close, momentumLen)  momentumLen

 Only allow one signal per box, on the box's first bar and only if momentum confirms
safeCount = array.size(voidBoxes)
if safeCount  0
    for i = 0 to safeCount - 1
        vb = array.get(voidBars, i)
        isLab = array.get(voidLabeled, i)
        if not isLab and bar_index == vb
            if body  avgWick and momentumSlope  momentumThresh
                label.new(bar_index, low-(avgWick2), LONG, color=color.new(color.lime, 0), textcolor=color.black, size=sigSize, style=label.style_label_up)
                array.set(voidLabeled, i, true)
            if body  avgWick and momentumSlope  -momentumThresh
                label.new(bar_index, high+(avgWick2), SHORT, color=color.new(color.red, 0), textcolor=color.white, size=sigSize, style=label.style_label_down)
                array.set(voidLabeled, i, true)


 === Clean, compliant alert logic at global scope ===
isFirstVoidBar = array.size(voidBoxes)  0 and array.size(voidLabeled)  0 and bar_index == array.get(voidBars, array.size(voidBoxes)-1) and not array.get(voidLabeled, array.size(voidLabeled)-1)
longAlert  = isFirstVoidBar and body  avgWick and momentumSlope  momentumThresh
shortAlert = isFirstVoidBar and body  avgWick and momentumSlope  -momentumThresh
alertcondition(longAlert,  title=Powerful Long Signal,  message=High probability LONG signal void + momentum up.)
alertcondition(shortAlert, title=Powerful Short Signal, message=High probability SHORT signal void + momentum down.)

 === Dashboard User-Selectable Position ===
pos = dashPos == top_right     position.top_right  dashPos == top_left      position.top_left  dashPos == bottom_right  position.bottom_right  position.bottom_left

voidCount = array.size(voidBoxes)
filled = 0
partial = 0
if voidCount  0
    for i = 0 to voidCount - 1
        vhigh = array.get(voidHighs, i)
        vlow  = array.get(voidLows, i)
        vbar  = array.get(voidBars, i)
        fill = false
        pfill = false
        barsChecked = math.min(bar_index - vbar, voidLength)
        if barsChecked  0
            for j = 1 to barsChecked
                idx = vbar + j
                hist_idx = idx - bar_index
                if hist_idx = 0
                    phigh = high[hist_idx]
                    plow  = low[hist_idx]
                    if phigh = vlow and plow = vhigh
                        fill = true
                    else
                        if (phigh = vlow and phigh = vhigh) or (plow = vlow and plow = vhigh)
                            pfill = true
            if fill
                filled = filled + 1
            else
                if pfill
                    partial = partial + 1

continuationProb = voidCount  0  (voidCount - filled - partial)  voidCount  0
partialProb      = voidCount  0  partial  voidCount  0
fillProb         = voidCount  0  filled  voidCount  0

var table dash = na
if showDashboard and na(dash)
    dash = table.new(pos, 1, 4)
if showDashboard
    table.cell(dash, 0, 0, Void Continuation, bgcolor=color.black, text_color=color.white)
    table.cell(dash, 0, 1, Unfilled  + str.tostring(continuationProb  100, #.0) + %, bgcolor=color.orange, text_color=color.black)
    table.cell(dash, 0, 2, Partial Fill  + str.tostring(partialProb  100, #.0) + %, bgcolor=color.yellow, text_color=color.black)
    table.cell(dash, 0, 3, Full Fill  + str.tostring(fillProb  100, #.0) + %, bgcolor=color.green, text_color=color.black)

 === Informational Disclaimer ===
if bar_index == 1
    label.new(bar_index, na, For context only. No trading advice., color=color.silver, style=label.style_label_left)